 .model tiny
 .386p

;// setup.s负责从BIOS 中获取系统数据，并将这些数据放到系统内存的适当地方。
;// 此时setup.s 和system 已经由bootsect 引导块加载到内存中。
;// 这段代码询问bios 有关内存/磁盘/其它参数，并将这些参数放到一个
;// “安全的”地方：90000-901FF，也即原来bootsect 代码块曾经在
;// 的地方，然后在被缓冲块覆盖掉之前由保护模式的system 读取。

;// 以下这些参数最好和bootsect.s 中的相同！
 INITSEG  = 9000h	;// 原来bootsect 所处的段
 SYSSEG   = 1000h	;// system 在10000(64k)处。
 SETUPSEG = 9020h	;// 本程序所在的段地址。


code segment
start:

;// ok, 整个读磁盘过程都正常，现在将光标位置保存以备今后使用。

	mov	ax,INITSEG		;// 将ds 置成INITSEG(9000)。这已经在bootsect 程序中
	mov	ds,ax			;// 设置过，但是现在是setup 程序，Linus 觉得需要再重新
						;// 设置一下。
	mov	ah,03h		;// BIOS 中断10 的读光标功能号ah = 03
	xor	bh,bh		;// 输入：bh = 页号
	int	10h			;// 返回：ch = 扫描开始线，cl = 扫描结束线，
	mov	ds:[0],dx		;// dh = 行号(00 是顶端)，dl = 列号(00 是左边)。
					;// 将光标位置信息存放在90000 处，控制台初始化时会来取。
	mov	ah,88h		;// 这3句取扩展内存的大小值（KB）。
	int	15h			;// 是调用中断15，功能号ah = 88
	mov	ds:[2],ax		;// 返回：ax = 从100000（1M）处开始的扩展内存大小(KB)。
					;// 若出错则CF 置位，ax = 出错码。

;// 下面这段用于取显示卡当前显示模式。
;// 调用BIOS 中断10，功能号ah = 0f
;// 返回：ah = 字符列数，al = 显示模式，bh = 当前显示页。
;// 90004(1 字)存放当前页，90006 显示模式，90007 字符列数。
	mov	ah,0fh
	int	10h
	mov	ds:[4],bx		;// bh = display page
	mov	ds:[6],ax		;// al = video mode, ah = window width

;// 检查显示方式（EGA/VGA）并取参数。
;// 调用BIOS 中断10，附加功能选择-取方式信息
;// 功能号：ah = 12，bl = 10
;// 返回：bh = 显示状态
;// (00 - 彩色模式，I/O 端口=3dX)
;// (01 - 单色模式，I/O 端口=3bX)
;// bl = 安装的显示内存
;// (00 - 64k, 01 - 128k, 02 - 192k, 03 = 256k)
;// cx = 显示卡特性参数(参见程序后的说明)。
	mov	ah,12h
	mov	bl,10h
	int	10h
	mov	ds:[8],ax		;// 90008 = ??
	mov	ds:[10],bx		;// 9000A = 安装的显示内存，9000B = 显示状态(彩色/单色)
	mov	ds:[12],cx		;// 9000C = 显示卡特性参数。

;// 取第一个硬盘的信息（复制硬盘参数表）。
;// 第1 个硬盘参数表的首地址竟然是中断向量41 的向量值！而第2 个硬盘
;// 参数表紧接第1 个表的后面，中断向量46 的向量值也指向这第2 个硬盘
;// 的参数表首址。表的长度是16 个字节(10)。
;// 下面两段程序分别复制BIOS 有关两个硬盘的参数表，90080 处存放第1 个
;// 硬盘的表，90090 处存放第2 个硬盘的表。
	mov	ax,0000h
	mov	ds,ax
	lds	si,ds:[4*41h]		;// 取中断向量41 的值，也即hd0 参数表的地址 ds:si
	mov	ax,INITSEG
	mov	es,ax
	mov	di,0080h		;// 传输的目的地址: 9000:0080 -> es:di
	mov	cx,10h			;// 共传输10 字节。
	rep movsb

;// Get hd1 data

	mov	ax,0000h
	mov	ds,ax
	lds	si,ds:[4*46h]		;// 取中断向量46 的值，也即hd1 参数表的地址 -> ds:si
	mov	ax,INITSEG
	mov	es,ax
	mov	di,0090h		;// 传输的目的地址: 9000:0090 -> es:di
	mov	cx,10h
	rep movsb
	

;// 检查系统是否存在第2 个硬盘，如果不存在则第2 个表清零。
;// 利用BIOS 中断调用13 的取盘类型功能。
;// 功能号ah = 15；
;// 输入：dl = 驱动器号（8X 是硬盘：80 指第1 个硬盘，81 第2 个硬盘）
;// 输出：ah = 类型码；00 --没有这个盘，CF 置位； 01 --是软驱，没有change-line 支持；
;//  02--是软驱(或其它可移动设备)，有change-line 支持； 03 --是硬盘。
	mov	ax,1500h
	mov	dl,81h
	int	13h
	jc	no_disk1
	cmp	ah,3			;// 是硬盘吗？(类型= 3 ？)。
	je	is_disk1
no_disk1:
	mov	ax,INITSEG		;// 第2个硬盘不存在，则对第2个硬盘表清零。
	mov	es,ax
	mov	di,0090h
	mov	cx,10h
	mov	ax,00h
	rep stosb
	
is_disk1:

;// 从这里开始我们要保护模式方面的工作了。

	cli			;// 此时不允许中断。 ;//

;// 首先我们将system 模块移到正确的位置。
;// bootsect 引导程序是将system 模块读入到从10000（64k）开始的位置。由于当时假设
;// system 模块最大长度不会超过80000（512k），也即其末端不会超过内存地址90000，
;// 所以bootsect 会将自己移动到90000 开始的地方，并把setup 加载到它的后面。
;// 下面这段程序的用途是再把整个system 模块移动到00000 位置，即把从10000 到8ffff
;// 的内存数据块(512k)，整块地向内存低端移动了10000（64k）的位置。

	mov	ax,0000h
	cld			;// 'direction'=0, movs moves forward
do_move:
	mov	es,ax		;// es:di -> 目的地址(初始为0000:0)
	add	ax,1000h
	cmp	ax,9000h	;// 已经把从8000 段开始的64k 代码移动完？
	jz	end_move
	mov	ds,ax		;// ds:si -> 源地址(初始为1000:0)
	sub	di,di
	sub	si,si
	mov cx,8000h	;// 移动8000 字（64k 字节）。
	rep movsw
	jmp	do_move

;// 此后，我们加载段描述符。
;// 从这里开始会遇到32 位保护模式的操作，因此需要Intel 32 位保护模式编程方面的知识了,
;// 有关这方面的信息请查阅列表后的简单介绍或附录中的详细说明。这里仅作概要说明。
;//
;// lidt 指令用于加载中断描述符表(idt)寄存器，它的操作数是6 个字节，0-1 字节是描述符表的
;// 长度值(字节)；2-5 字节是描述符表的32 位线性基地址（首地址），其形式参见下面
;// 219-220 行和223-224 行的说明。中断描述符表中的每一个表项（8 字节）指出发生中断时
;// 需要调用的代码的信息，与中断向量有些相似，但要包含更多的信息。
;//
;// lgdt 指令用于加载全局描述符表(gdt)寄存器，其操作数格式与lidt 指令的相同。全局描述符
;// 表中的每个描述符项(8 字节)描述了保护模式下数据和代码段（块）的信息。其中包括段的
;// 最大长度限制(16 位)、段的线性基址（32 位）、段的特权级、段是否在内存、读写许可以及
;// 其它一些保护模式运行的标志。参见后面205-216 行。
;//

end_move:
	mov	ax,SETUPSEG		;// right, forgot this at first. didn't work :-)
	mov	ds,ax			;// ds 指向本程序(setup)段。因为上面操作改变了ds的值。

	lidt fword ptr idt_48			;// 加载中断描述符表(idt)寄存器，idt_48 是6 字节操作数的位置
						;// 前2 字节表示idt 表的限长，后4 字节表示idt 表所处的基地址。

	lgdt fword ptr gdt_48			;// 加载全局描述符表(gdt)寄存器，gdt_48 是6 字节操作数的位置

;// 以上的操作很简单，现在我们开启A20 地址线。

	call empty_8042		;// 等待输入缓冲器空。
						;// 只有当输入缓冲器为空时才可以对其进行写命令。
	mov	al,0D1h			;// D1 命令码-表示要写数据到8042 的P2 端口。P2 端
	out	64h,al			;// 口的位1 用于A20 线的选通。数据要写到60 口。

	call empty_8042		;// 等待输入缓冲器空，看命令是否被接受。
	mov	al,0DFh			;// A20 on 选通A20 地址线的参数。
	out	60h,al
	call empty_8042		;// 输入缓冲器为空，则表示A20 线已经选通。

;// 希望以上一切正常。现在我们必须重新对中断进行编程 
;// 我们将它们放在正好处于intel 保留的硬件中断后面，在int 20-2F。
;// 在那里它们不会引起冲突。不幸的是IBM 在原PC 机中搞糟了，以后也没有纠正过来。
;// PC 机的bios 将中断放在了08-0f，这些中断也被用于内部硬件中断。
;// 所以我们就必须重新对8259 中断控制器进行编程，这一点都没劲。

	mov	al,11h		;// 11 表示初始化命令开始，是ICW1 命令字，表示边
					;// 沿触发、多片8259 级连、最后要发送ICW4 命令字。
	out	20h,al		;// 发送到8259A 主芯片。
	dw	00ebh,00ebh		;// jmp $+2, jmp $+2  $ 表示当前指令的地址，
								;// 两条跳转指令，跳到下一条指令，起延时作用。
	out	0A0h,al		;// and to 8259A-2 ;// 再发送到8259A 从芯片。
	dw	00ebh,00ebh
	mov	al,20h		;// start of hardware int's (20)
	out	21h,al		;// 送主芯片ICW2 命令字，起始中断号，要送奇地址。
	dw	00ebh,00ebh
	mov	al,28h		;// start of hardware int's 2 (28)
	out	0A1h,al		;// 送从芯片ICW2 命令字，从芯片的起始中断号。
	dw	00ebh,00ebh
	mov	al,04h		;// 8259-1 is master
	out	21h,al		;// 送主芯片ICW3 命令字，主芯片的IR2 连从芯片INT。
	dw	00ebh,00ebh	;// 参见代码列表后的说明。
	mov	al,02h		;// 8259-2 is slave
	out	0A1h,al		;// 送从芯片ICW3 命令字，表示从芯片的INT 连到主芯
						;// 片的IR2 引脚上。
	dw	00ebh,00ebh
	mov	al,01h		;// 8086 mode for both
	out	21h,al		;// 送主芯片ICW4 命令字。8086 模式；普通EOI 方式，
						;// 需发送指令来复位。初始化结束，芯片就绪。
	dw	00ebh,00ebh
	out	0A1h,al		;// 送从芯片ICW4 命令字，内容同上。
	dw	00ebh,00ebh
	mov	al,0FFh		;// mask off all interrupts for now
	out	21h,al		;// 屏蔽主芯片所有中断请求。
	dw	00ebh,00ebh
	out	0A1h,al		;// 屏蔽从芯片所有中断请求。

;// 哼，上面这段当然没劲 ，希望这样能工作，而且我们也不再需要乏味的BIOS 了（除了
;// 初始的加载.。BIOS 子程序要求很多不必要的数据，而且它一点都没趣。那是“真正”的
;// 程序员所做的事。

;// 这里设置进入32 位保护模式运行。首先加载机器状态字(lmsw - Load Machine Status Word)，
;// 也称控制寄存器CR0，其比特位0 置1 将导致CPU 工作在保护模式。

	mov	ax,0001h	;// 保护模式比特位(PE)。
	lmsw ax			;// 就这样加载机器状态字
;	jmp 8:0  		;// 跳转至cs 段8，偏移0 处。执行system 中的代码
	db 0eah
	dw 0
	dw 8
;// 我们已经将system 模块移动到00000 开始的地方，所以这里的偏移地址是0。这里的段值
;// 的8 已经是保护模式下的段选择符了，用于选择描述符表和描述符表项以及所要求的特权级。
;// 段选择符长度为16 位（2 字节）；位0-1 表示请求的特权级0-3，linux 操作系统只用
;// 到两级：0 级（系统级）和3 级（用户级）；位2 用于选择全局描述符表(0)还是局部描
;// 述符表(1)；位3-15 是描述符表项的索引，指出选择第几项描述符。所以段选择符
;// 8(00000,0000,0000,1000)表示请求特权级0、使用全局描述符表中的第1 项，该项指出
;// 代码的基地址是0，因此这里的跳转指令就会去执行system 中的代码。


;// 下面这个子程序检查键盘命令队列是否为空。这里不使用超时方法- 如果这里死机，
;// 则说明PC 机有问题，我们就没有办法再处理下去了。
;// 只有当输入缓冲器为空时（状态寄存器位2 = 0）才可以对其进行写命令。
empty_8042:
	dw 00ebh,00ebh	;// jmp $+2, jmp $+2 $ 表示当前指令的地址
						;// 这是两个跳转指令的机器码(跳转到下一句)，相当于延时空操作。
	in	al,64h			;// 读AT 键盘控制器状态寄存器。
	test al,2			;// 测试位2，输入缓冲器满？
	jnz	empty_8042		;// yes - loop
	ret

;// 全局描述符表开始处。描述符表由多个8 字节长的描述符项组成。
;// 这里给出了3 个描述符项。第1 项无用，但须存在。第2 项是系统代码段
;// 描述符（208-211 行），第3 项是系统数据段描述符(213-216 行)。每个描述符的具体
;// 含义参见列表后说明。
gdt:
	dw	0,0,0,0		;// 第1 个描述符，不用。
;// 这里在gdt 表中的偏移量为08，当加载代码段寄存器(段选择符)时，使用的是这个偏移值。
	dw	07FFh		;// 8Mb - limit=2047 (2048*4096=8Mb)
	dw	0000h		;// base address=0
	dw	9A00h		;// code read/exec
	dw	00C0h		;// granularity=4096, 386
;// 这里在gdt 表中的偏移量是10，当加载数据段寄存器(如ds 等)时，使用的是这个偏移值。
	dw	07FFh		;// 8Mb - limit=2047 (2048*4096=8Mb)
	dw	0000h		;// base address=0
	dw	9200h		;// data read/write
	dw	00C0h		;// granularity=4096, 386

idt_48:
	dw	0			;// idt limit=0
	dw	0,0			;// idt base=0L

gdt_48:
	dw	800h		;// 全局表长度为2k 字节，因为每8 字节组成一个段描述符项
						;// 所以表中共可有256 项。
	dw	512+gdt,9h	;// 4 个字节构成的内存线性地址：0009<<16 + 0200+gdt
						;// 也即90200 + gdt(即在本程序段中的偏移地址，205 行)。
	
code ends
end